﻿/*
 * Code slightly modified from that posted at http://www.gamedev.net/topic/457783-xna-getting-text-from-keyboard/
 * by users "Promit Roy" and "Nypyren"
 * 
 * */


using System;
using System.Runtime.InteropServices;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;

namespace XRGLib
{

    /// <summary>
    /// Information about a character key down event (corresponds to a Windows WM_CHAR message)
    /// </summary>
    public class CharacterEventArgs : EventArgs
    {
        private readonly char character;
        private readonly int lParam;

        internal CharacterEventArgs(char character, int lParam)
        {
            this.character = character; 
            this.lParam = lParam;
        }

        /// <summary>
        /// The character code of the key
        /// </summary>
        public char Character 
        { 
            get 
            { 
                return character;
            }
        }

        /// <summary>
        /// The Windows "lParam" of the WM_CHAR message
        /// </summary>
        public int Param 
        { 
            get
            { 
                return lParam;
            } 
        }

        /// <summary>
        /// The repeat count for the current message. The value is the number of times the keystroke is autorepeated 
        /// as a result of the user holding down the key. If the keystroke is held long enough, multiple messages are sent. 
        /// However, the repeat count is not cumulative.
        /// </summary>
        public int RepeatCount 
        { 
            get 
            { 
                return lParam & 0xffff; 
            } 
        }

        /// <summary>
        /// Indicates whether the key is an extended key, such as the right-hand ALT and CTRL keys that appear on an 
        /// enhanced 101- or 102-key keyboard.
        /// </summary>
        public bool ExtendedKey
        {
            get 
            { 
                return (lParam & (1 << 24)) > 0; 
            }
        }

        /// <summary>
        /// Indicates whether the ALT key is held down while the key is pressed.
        /// </summary>
        public bool AltPressed
        {
            get 
            { 
                return (lParam & (1 << 29)) > 0; 
            }
        }

        /// <summary>
        /// The previous key state. True if the key is down before the message is sent, or false if the key is up.
        /// </summary>
        public bool PreviousState
        {
            get 
            { 
                return (lParam & (1 << 30)) > 0; 
            }
        }

        /// <summary>
        /// The transition state. True if the key is being released, or false if the key is being pressed.
        /// </summary>
        public bool TransitionState
        {
            get
            { 
                return (lParam & (1 << 31)) > 0; 
            }
        }

    }

    /// <summary>
    /// Information about a key event (corresponds to a Windows WM_KEYDOWN message)
    /// </summary>
    public class KeyEventArgs : EventArgs
    {
        private Keys keyCode;

        internal KeyEventArgs(Keys keyCode)
        {
            this.keyCode = keyCode;
        }		

        /// <summary>
        /// The virtual key code
        /// </summary>
        public Keys KeyCode 
        {
            get 
            { 
                return keyCode;
            }
        }
    }

    /// <summary>
    /// Information about a mouse event.
    /// </summary>
    public class MouseEventArgs : EventArgs
    {
        private MouseButton button; 
        private int clicks;
        private int x; private int y; 
        private int delta;

        /// <summary>
        /// The button being pressed, if any
        /// </summary>
        public MouseButton Button
        {
            get
            {
                return button;
            }
        }

        /// <summary>
        /// Number of clicks the button has been pushed
        /// </summary>
        public int Clicks 
        { 
            get 
            { 
                return clicks; 
        
            } 
        }

        /// <summary>
        ///  Coordinate relative to upper left corner of window (in pixels)
        /// </summary>
        public int X
        { 
            get 
            { return x; } 
        }

        /// <summary>
        /// Coordinate relative to upper left corner of window (in pixels)
        /// </summary>
        public int Y
        {
            get
            {
                return y;
            }
        }

        /// <summary>
        /// Coordinates relative to upper left corner of window (in pixels)
        /// </summary>
        public Point Location 
        { 
            get 
            { 
                return new Point(x, y); 
            } 
        }

        /// <summary>
        /// The delta of the middle scroll wheel
        /// </summary>
        public int Delta
        { 
            get
            { 
                return delta; 
            } 
        }

        internal MouseEventArgs(MouseButton button, int clicks, int x, int y, int delta)
        {
            this.button = button; this.clicks = clicks; this.x = x; this.y = y;
            this.delta = delta;
        }
    }

    /// <summary>	
    /// Mouse Key Flags from WinUser.h for mouse related WM messages.	
    /// </summary>	
    [Flags]
    public enum MouseKeys
    {
        LButton = 0x01, RButton = 0x02, Shift = 0x04,
        Control = 0x08, MButton = 0x10, XButton1 = 0x20, XButton2 = 0x40
    }

    /// <summary>
    /// Corresponds to a button on the mouse
    /// </summary>
    public enum MouseButton { None, Left, Right, Middle, X1, X2 }

    /// <summary>
    /// A utility class that provides a way to hook into the Windows message system.
    /// </summary>
    /// <remarks>
    /// This class provides events to subscribe to for various Windows input events.  The major benefit of using this
    /// over XNA state polling is that Windows intelligently converts keystrokes into characters for us, which is otherwise not a
    /// trivial task (due to different localization settings, keyboard types, etc.).<para/>
    /// To use, simply call Initialized at startup, and then subscribe/unsubscribe to the events as needed.  InputSystem automatcally
    /// hooks into the Windows messenging system.
    /// </remarks>
    public static class InputSystem
    {
        #region Events

        /// <summary>		
        /// Event raised when a character has been entered.		
        /// </summary>		
        public static event EventHandler<CharacterEventArgs> CharEntered;
        /// <summary>		
        /// Event raised when a key has been pressed down. May fire multiple times due to keyboard repeat.		
        /// </summary>		
        public static event EventHandler<KeyEventArgs> KeyDown;
        /// <summary>		
        /// Event raised when a key has been released.		
        /// </summary>		
        public static event EventHandler<KeyEventArgs> KeyUp;
        /// <summary>		
        /// Event raised when a mouse button is pressed.		
        /// </summary>		
        public static event EventHandler<MouseEventArgs> MouseDown;
        /// <summary>	
        /// /// Event raised when a mouse button is released.		
        /// </summary>		
        public static event EventHandler<MouseEventArgs> MouseUp;
        /// <summary>		
        /// Event raised when the mouse changes location.		
        /// </summary>		
        public static event EventHandler<MouseEventArgs> MouseMove;
        /// <summary>	
        /// /// Event raised when the mouse has hovered in the same location for a short period of time.		
        /// </summary>		
        public static event EventHandler<MouseEventArgs> MouseHover;
        /// <summary>		
        /// Event raised when the mouse wheel has been moved.	
        /// /// </summary>		
        public static event EventHandler<MouseEventArgs> MouseWheel;
        /// <summary>	
        /// /// Event raised when a mouse button has been double clicked.	
        /// </summary>		
        public static event EventHandler<MouseEventArgs> MouseDoubleClick;

        #endregion

        delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
        static bool initialized;
        static IntPtr prevWndProc;
        static WndProc hookProcDelegate;
        static IntPtr hIMC;

        #region Win32 Constants
        const int GWL_WNDPROC = -4;
        const int WM_KEYDOWN = 0x100;
        const int WM_KEYUP = 0x101;
        const int WM_CHAR = 0x102;
        const int WM_IME_SETCONTEXT = 0x281;
        const int WM_INPUTLANGCHANGE = 0x51;
        const int WM_GETDLGCODE = 0x87;
        const int WM_IME_COMPOSITION = 0x10F;
        const int DLGC_WANTALLKEYS = 4;
        const int WM_MOUSEMOVE = 0x200;
        const int WM_LBUTTONDOWN = 0x201;
        const int WM_LBUTTONUP = 0x202;
        const int WM_LBUTTONDBLCLK = 0x203;
        const int WM_RBUTTONDOWN = 0x204;
        const int WM_RBUTTONUP = 0x205;
        const int WM_RBUTTONDBLCLK = 0x206;
        const int WM_MBUTTONDOWN = 0x207;
        const int WM_MBUTTONUP = 0x208;
        const int WM_MBUTTONDBLCLK = 0x209;
        const int WM_MOUSEWHEEL = 0x20A;
        const int WM_XBUTTONDOWN = 0x20B;
        const int WM_XBUTTONUP = 0x20C;
        const int WM_XBUTTONDBLCLK = 0x20D;
        const int WM_MOUSEHOVER = 0x2A1;
        #endregion

        #region DLL Imports

        [DllImport("Imm32.dll")]
        static extern IntPtr ImmGetContext(IntPtr hWnd);
        [DllImport("Imm32.dll")]
        static extern IntPtr ImmAssociateContext(IntPtr hWnd, IntPtr hIMC);
        [DllImport("user32.dll")]
        static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
        [DllImport("user32.dll")]
        static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);

        #endregion

        /// <summary>
        /// Gets the current location of the mouse pointer, in pixel coordinates relative to the upper left
        /// corner of the window.
        /// </summary>
        public static Point MouseLocation
        {
            get
            {
                MouseState state = Mouse.GetState();
                return new Point(state.X, state.Y);
            }
        }

        /// <summary>
        /// True if the shift key is being held down
        /// </summary>
        public static bool ShiftDown
        {
            get
            {
                KeyboardState state = Keyboard.GetState();
                return state.IsKeyDown(Keys.LeftShift) || state.IsKeyDown(Keys.RightShift);
            }
        }

        /// <summary>
        /// True if the control key is being held down
        /// </summary>
        public static bool CtrlDown
        {
            get
            {
                KeyboardState state = Keyboard.GetState(); return state.IsKeyDown(Keys.LeftControl) || state.IsKeyDown(Keys.RightControl);
            }
        }

        /// <summary>
        /// True if the Alt key is being held down
        /// </summary>
        public static bool AltDown
        {
            get
            {
                KeyboardState state = Keyboard.GetState();
                return state.IsKeyDown(Keys.LeftAlt) || state.IsKeyDown(Keys.RightAlt);
            }
        }

        /// <summary>		
        /// Initialize the TextInput with the given GameWindow.		
        /// </summary>		
        /// <param name="window">The XNA window to which text input should be linked.</param>	
        public static void Initialize(GameWindow window)
        {
            if (initialized)
                throw new InvalidOperationException("TextInput.Initialize can only be called once!");
            hookProcDelegate = new WndProc(HookProc);
            prevWndProc = (IntPtr)SetWindowLong(window.Handle, GWL_WNDPROC, (int)Marshal.GetFunctionPointerForDelegate(hookProcDelegate));
            hIMC = ImmGetContext(window.Handle); 
            initialized = true;
        }

        static IntPtr HookProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
        {
            IntPtr returnCode = CallWindowProc(prevWndProc, hWnd, msg, wParam, lParam);
            switch (msg)
            {
                case WM_GETDLGCODE:
                    returnCode = (IntPtr)(returnCode.ToInt32() | DLGC_WANTALLKEYS);
                    break;

                case WM_KEYDOWN: 
                    if (KeyDown != null)
                        KeyDown(null, new KeyEventArgs((Keys)wParam)); 
                        break;

                case WM_KEYUP: 
                    if (KeyUp != null)
                        KeyUp(null, new KeyEventArgs((Keys)wParam));
                    break;

                case WM_CHAR:
                    if (CharEntered != null)
                        CharEntered(null, new CharacterEventArgs((char)wParam, lParam.ToInt32()));
                    break;

                case WM_IME_SETCONTEXT:
                    if (wParam.ToInt32() == 1)
                        ImmAssociateContext(hWnd, hIMC);
                    break;

                case WM_INPUTLANGCHANGE:
                    ImmAssociateContext(hWnd, hIMC);
                    returnCode = (IntPtr)1;
                    break;

                // Mouse messages				
                case WM_MOUSEMOVE:
                    if (MouseMove != null)
                    {
                        short x, y;
                        MouseLocationFromLParam(lParam.ToInt32(), out x, out y);
                        MouseMove(null, new MouseEventArgs(MouseButton.None, 0, x, y, 0));
                    }
                    break;

                case WM_MOUSEHOVER:
                    if (MouseHover != null)
                    {
                        short x, y;
                        MouseLocationFromLParam(lParam.ToInt32(), out x, out y);
                        MouseHover(null, new MouseEventArgs(MouseButton.None, 0, x, y, 0));
                    } 
                    break;

                case WM_MOUSEWHEEL:
                    if (MouseWheel != null)
                    {
                        short x, y;
                        MouseLocationFromLParam(lParam.ToInt32(), out x, out y);
                        MouseWheel(null, new MouseEventArgs(MouseButton.None, 0, x, y, (wParam.ToInt32() >> 16) / 120));
                    }
                    break;

                case WM_LBUTTONDOWN:
                    RaiseMouseDownEvent(MouseButton.Left, wParam.ToInt32(), lParam.ToInt32()); 
                    break;

                case WM_LBUTTONUP:
                    RaiseMouseUpEvent(MouseButton.Left, wParam.ToInt32(), lParam.ToInt32()); 
                    break;

                case WM_LBUTTONDBLCLK:
                    RaiseMouseDblClickEvent(MouseButton.Left, wParam.ToInt32(), lParam.ToInt32()); 
                    break;

                case WM_RBUTTONDOWN: 
                    RaiseMouseDownEvent(MouseButton.Right, wParam.ToInt32(), lParam.ToInt32()); 
                    break;

                case WM_RBUTTONUP:
                    RaiseMouseUpEvent(MouseButton.Right, wParam.ToInt32(), lParam.ToInt32()); 
                    break;

                case WM_RBUTTONDBLCLK:
                    RaiseMouseDblClickEvent(MouseButton.Right, wParam.ToInt32(), lParam.ToInt32()); 
                    break;

                case WM_MBUTTONDOWN: 
                    RaiseMouseDownEvent(MouseButton.Middle, wParam.ToInt32(), lParam.ToInt32()); 
                    break;

                case WM_MBUTTONUP: 
                    RaiseMouseUpEvent(MouseButton.Middle, wParam.ToInt32(), lParam.ToInt32());
                    break;

                case WM_MBUTTONDBLCLK: 
                    RaiseMouseDblClickEvent(MouseButton.Middle, wParam.ToInt32(), lParam.ToInt32()); 
                    break;

                case WM_XBUTTONDOWN: 
                    if ((wParam.ToInt32() & 0x10000) != 0)
                    {
                        RaiseMouseDownEvent(MouseButton.X1, wParam.ToInt32(), lParam.ToInt32());
                    }
                    else if ((wParam.ToInt32() & 0x20000) != 0)
                    {
                        RaiseMouseDownEvent(MouseButton.X2, wParam.ToInt32(), lParam.ToInt32());
                    }
                    break;

                case WM_XBUTTONUP: if ((wParam.ToInt32() & 0x10000) != 0)
                    {
                        RaiseMouseUpEvent(MouseButton.X1, wParam.ToInt32(), lParam.ToInt32());
                    }
                    else if ((wParam.ToInt32() & 0x20000) != 0)
                    {
                        RaiseMouseUpEvent(MouseButton.X2, wParam.ToInt32(), lParam.ToInt32());
                    }
                    break;

                case WM_XBUTTONDBLCLK: if ((wParam.ToInt32() & 0x10000) != 0)
                    {
                        RaiseMouseDblClickEvent(MouseButton.X1, wParam.ToInt32(), lParam.ToInt32());
                    }
                    else if ((wParam.ToInt32() & 0x20000) != 0)
                    {
                        RaiseMouseDblClickEvent(MouseButton.X2, wParam.ToInt32(), lParam.ToInt32());
                    }
                    break;

            } 
            return returnCode;
        }

        #region Mouse Message Helpers

        static void RaiseMouseDownEvent(MouseButton button, int wParam, int lParam)
        {
            if (MouseDown != null)
            {
                short x, y; 
                MouseLocationFromLParam(lParam, out x, out y);
                MouseDown(null, new MouseEventArgs(button, 1, x, y, 0));
            }
        }

        static void RaiseMouseUpEvent(MouseButton button, int wParam, int lParam)
        {
            if (MouseUp != null)
            {
                short x, y;
                MouseLocationFromLParam(lParam, out x, out y);
                MouseUp(null, new MouseEventArgs(button, 1, x, y, 0));
            }
        }

        static void RaiseMouseDblClickEvent(MouseButton button, int wParam, int lParam)
        {
            if (MouseDoubleClick != null)
            {
                short x, y;
                MouseLocationFromLParam(lParam, out x, out y);
                MouseDoubleClick(null, new MouseEventArgs(button, 1, x, y, 0));
            }
        }

        static void MouseLocationFromLParam(int lParam, out short x, out short y)
        {
            // Cast to signed shorts to get sign extension on negative coordinates (of course this would only be possible if mouse capture was enabled).
            x = (short)(lParam & 0xFFFF);
            y = (short)(lParam >> 16);
        }

        #endregion
    }
}